package com.carleasoft.mps.security.permission.access.security;

import com.alibaba.fastjson.JSON;
import com.auth0.jwt.exceptions.SignatureVerificationException;
import com.auth0.jwt.exceptions.TokenExpiredException;
import com.carleasoft.mps.core.constant.ErrorInfoEnum;
import com.carleasoft.mps.core.perm.DataPermCache;
import com.carleasoft.mps.core.perm.ThreadPermDataContext;
import com.carleasoft.mps.core.result.ResultBody;
import com.carleasoft.mps.core.user.ThreadUserContext;
import com.carleasoft.mps.core.user.UserCacheData;
import com.carleasoft.mps.data.redis.JedisService;
import com.carleasoft.mps.data.redis.base.CacheKeyEnum;
import com.carleasoft.mps.security.permission.constants.SecurityConsts;
import com.carleasoft.mps.security.permission.feign.RemoteAllCacheService;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.RequestMethod;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.HashMap;
import java.util.Map;

public class PcJwtFilter extends BasicHttpAuthenticationFilter {

    private Logger logger = LoggerFactory.getLogger(this.getClass());

    JwtProperties jwtProperties;
    JedisService jedisService;
    RemoteAllCacheService remoteAllCacheService;

    public PcJwtFilter(JwtProperties jwtProperties, JedisService jedisService,RemoteAllCacheService remoteAllCacheService) {
        this.jwtProperties = jwtProperties;
        this.jedisService = jedisService;
        this.remoteAllCacheService=remoteAllCacheService;
    }

    /**
     * 检测Header里是否有token字段
     * 判断是否登录
     */
    @Override
    protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {
        HttpServletRequest req = (HttpServletRequest) request;
        String authorization = req.getHeader(SecurityConsts.REQUEST_TOKEN_HEADER);
        return authorization != null;
    }

    /**
     * 登录验证
     *
     * @param request
     * @param response
     * @return
     * @throws Exception
     */
    @Override
    protected boolean executeLogin(ServletRequest request, ServletResponse response) {
//        logger.info("调用executeLogin验证登录");

        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        String authorization = httpServletRequest.getHeader(SecurityConsts.REQUEST_TOKEN_HEADER);
        //校验token合法性
        JwtToken jwttoken = new JwtToken(authorization);
        // 如果没有抛出异常则代表登入成功，返回true,执行realm的doGetAuthenticationInfo方法
        getSubject(request, response).login(jwttoken);
        //进行权限检查，执行realm的doGetAuthorizationInfo
        getSubject(request, response).isPermitted();
        //绑定上下文获取账号,从redis中取出最新的用户信息绑定到线程上下文中,如果没有获取到信息，则直接重回登录
        String account = JwtUtil.getClaim(authorization, SecurityConsts.ACCOUNT);
        this.bindCheck(account);
        //刷新token,在后台把手机账号和数据权限改变的情况下，token会强制清空，重新走登录流程
        this.refreshTokenIfNeed(account, authorization, response);
        // 提交给realm进行验证权限，如果错误抛出异常并被捕获

        return true;
    }

    /**
     * 检查是否需要,若需要则校验时间戳，刷新Token，并更新时间戳
     *
     * @param account
     * @param authorization
     * @param response
     * @return
     */
    private boolean refreshTokenIfNeed(String account, String authorization, ServletResponse response) {
        Long currentTimeMillis = System.currentTimeMillis();
        //检查刷新规则
        if (this.refreshCheck(authorization, currentTimeMillis)) {

            if (jedisService.exists(CacheKeyEnum.PREFIX_SHIRO_REFRESH_TOKEN,account)) {
                //检查redis中的时间戳与token的时间戳是否一致，判断token是否过期需要重新登录
                String tokenTimeStamp = jedisService.getString(CacheKeyEnum.PREFIX_SHIRO_REFRESH_TOKEN,account);
                String tokenMillis = JwtUtil.getClaim(authorization, SecurityConsts.CURRENT_TIME_MILLIS);
                if (!tokenMillis.equals(tokenTimeStamp)) {
                    throw new TokenExpiredException(String.format("token过期", account));
                }
            }
            //时间戳一致，则颁发新的令牌
            logger.info(String.format("更换新的生成新的token", account));
            String strCurrentTimeMillis = String.valueOf(currentTimeMillis);
            String newToken = JwtUtil.sign(account, strCurrentTimeMillis);

            //更新redis中的token时间戳
            jedisService.addString(CacheKeyEnum.PREFIX_SHIRO_REFRESH_TOKEN,account,strCurrentTimeMillis, jwtProperties.getTokenExpireTime() * 60);

            HttpServletResponse httpServletResponse = (HttpServletResponse) response;
            httpServletResponse.setHeader(SecurityConsts.RESPONSE_TOKEN_HEADER, newToken);
            httpServletResponse.setHeader("Access-Control-Expose-Headers", SecurityConsts.RESPONSE_TOKEN_HEADER);


        }
        return true;
    }

    /**
     * 根据登录账号获取用户缓存数据，获取不到则重新走登录
     * 因为移动端缓存登录token时间比较长
     * @param account
     * @return
     */
    private void bindCheck(String account) {
        //对权限要求需要实时响应的，且权限容易经常调整的，需要实时调用权限接口，否则采用缓存存储，避免每次接口访问都需要从数据库查询计算
//        AllCache allCache=remoteAllCacheService.buildUser(account).getData();

        DataPermCache dataPermCache =(DataPermCache)jedisService.getObject(CacheKeyEnum.PREFIX_PERMS_CACHE_DATA,account);
        UserCacheData userCacheData = (UserCacheData)jedisService.getObject(CacheKeyEnum.PREFIX_UESRCACHE,account);
        if(dataPermCache==null || userCacheData==null){
            throw new TokenExpiredException(String.format("token过期", account));
        }else {
            ThreadPermDataContext.set(dataPermCache);
            ThreadUserContext.set(userCacheData);
        }
    }

    /**
     * 检查是否需要更新Token,根据token生成时间和有效期进行比对
     *
     * @param authorization
     * @param currentTimeMillis
     * @return
     */
    private boolean refreshCheck(String authorization, Long currentTimeMillis) {
        String tokenMillis = JwtUtil.getClaim(authorization, SecurityConsts.CURRENT_TIME_MILLIS);
        if (currentTimeMillis - Long.parseLong(tokenMillis) > (jwtProperties.refreshCheckTime * 60 * 1000L)) {
            return true;
        }
        return false;
    }

    /**
     * 是否允许访问
     *
     * @param request
     * @param response
     * @param mappedValue
     * @return
     */
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        if (isLoginAttempt(request, response)) {
            try {
                this.executeLogin(request, response);
                return true;
            } catch (Exception e) {
                ResultBody resultBody = null;

                if (e != null && e instanceof SignatureVerificationException) {
                    resultBody = new ResultBody(ErrorInfoEnum.ACCESS_NO);
                } else if (e != null && e instanceof TokenExpiredException) {
                    resultBody = new ResultBody(ErrorInfoEnum.TOKEN_TIMEOUT);
                } else if (e != null && e instanceof AuthenticationException) {
                    resultBody = new ResultBody(ErrorInfoEnum.TOKEN_TIMEOUT);
                } else {
                    resultBody = new ResultBody(ErrorInfoEnum.TOKEN_TIMEOUT);
                }
                this.response401(response, resultBody);
                return false;
            }
        } else {
            ResultBody resultBody = new ResultBody(ErrorInfoEnum.TOKEN_TIMEOUT);
            this.response401(response, resultBody);
            return false;
        }
    }

    /**
     * 重写 onAccessDenied 方法，避免父类中调用再次executeLogin
     *
     * @param request
     * @param response
     * @return
     */
    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) {
//        logger.info("调用onAccessDenied拒绝访问");
        this.sendChallenge(request, response);
        return false;
    }

    /**
     * 401非法请求
     *
     * @param resp
     * @param resultBody
     */
    private void response401(ServletResponse resp, ResultBody resultBody) {
        HttpServletResponse httpServletResponse = (HttpServletResponse) resp;
        httpServletResponse.setStatus(HttpStatus.OK.value());
        httpServletResponse.setCharacterEncoding("UTF-8");
        httpServletResponse.setContentType("application/json; charset=utf-8");
        Map res = new HashMap();
        res.put("code", resultBody.getCode());
        res.put("message", resultBody.getMessage());
        PrintWriter out = null;
        try {
            out = httpServletResponse.getWriter();
            out.append(JSON.toJSONString(res));
        } catch (IOException e) {
            logger.error("返回Response信息出现IOException异常:" + e.getMessage());
        } finally {
            if (out != null) {
                out.close();
            }
        }
    }

    /**
     * @param request 再请求完成后清空上下文usercache
     */
    @Override
    public void afterCompletion(ServletRequest request, ServletResponse response, Exception exception) throws Exception {
        ThreadUserContext.set(null);
        ThreadPermDataContext.set(null);
    }

    /**
     * 对跨域提供支持
     */
    @Override
    protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        HttpServletResponse httpServletResponse = (HttpServletResponse) response;
        httpServletResponse.setHeader("Access-control-Allow-Origin", httpServletRequest.getHeader("Origin"));
        httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET,POST");
        httpServletResponse.setHeader("Access-Control-Allow-Headers", httpServletRequest.getHeader("Access-Control-Request-Headers"));
        httpServletResponse.setHeader("Access-Control-Allow-Credentials", "true");
        // 跨域时会首先发送一个option请求，这里我们给option请求直接返回正常状态
        if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
            httpServletResponse.setStatus(HttpStatus.OK.value());
            return false;
        }
        return super.preHandle(request, response);
    }

}
